Golang

Field Value
Version 1.0.0
Status Draft
Author Datta Bhise
Target All Go Backend Engineering Teams

1. Introduction & Philosophy

The primary goal of this document is to ensure code consistency, maintainability, and simplicity. Go is an opinionated language; we embrace its idioms rather than fighting them.

Core Tenets:

  1. Clear is better than clever.
  2. Errors are values and must be handled explicitly.
  3. Concurrency is a tool, not a default state.
  4. Documentation is part of the code, not an afterthought.

2. Project Layout & Structure

We adhere to the community-standard Go Project Layout.

| Directory | Purpose |
| /cmd      | Main applications. Directory names match the binary (e.g., cmd/api-server). |
| /internal | Private application and library code. Compiler-enforced privacy. |
| /pkg      | Library code safe for external applications to import. |
| /api      | API protocols (Swagger/OpenAPI, Protocol Buffers, gRPC definitions). |
| /configs  | Configuration file templates or default configs. |
| /scripts  | Scripts to build, install, analyze, etc. |

Rule: Do not place application logic in the root directory. Keep the root for meta-files (go.mod, Dockerfile, README.md).

3. Formatting & Style

3.1 Automated Formatting

  • All code must be formatted using gofmt (or goimports).
  • This should be enforced via a pre-commit hook or CI pipeline.

3.2 Imports

Imports are grouped into three blocks, separated by newlines:

  1. Standard Library
  2. Third-party packages
  3. Internal/Company packages
import (
    "fmt"
    "os"

    "[github.com/gin-gonic/gin](https://github.com/gin-gonic/gin)"

    "[github.com/myorg/project/internal/user](https://github.com/myorg/project/internal/user)"
)

3.3 Line Length

  • Avoid lines longer than 120 characters.
  • Break long function signatures or boolean conditions into multiple lines for readability.

4. Naming Conventions

4.1 Packages

  • Single word, lowercase. (e.g., user, account, not user_service or User).
  • Package names should describe what is provided, not what it contains (avoid util, common, helper).

4.2 Variables

  • Short Scope = Short Name: Use i for loop indices, r for readers.
  • Long Scope = Descriptive Name: Exported variables or those used across large functions need explicit names (e.g., RequestTimeoutDuration).
  • MixedCaps: Use CamelCase. No snake_case.

4.3 Interfaces

  • One-method interfaces end in -er (e.g., Reader, Writer, Formatter).
  • Keep interfaces small (1-3 methods).

4.4 Getters

  • Go does not use get in getter names.
  • Bad: func (u *User) GetName() string
  • Good: func (u *User) Name() string

5. Architecture & Design patterns

5.1 Dependency Injection

Avoid global state. Dependencies should be injected explicitly, typically via the constructor.

Bad (Global State):

func CreateUser() {
    db.Execute(...) // "db" is a global variable
}


Good (Dependency Injection):
type Service struct {
    repo UserRepository
}

func NewService(r UserRepository) *Service {
    return &Service{repo: r}
}

5.2 Interfaces: Consumer Defined

Define interfaces where they are used, not where they are implemented. This reduces coupling.

  • Accept Interfaces, Return Structs: Functions should accept the broadest possible interface (behavior) and return concrete types (data).

5.3 Configuration

  • Use a struct-based configuration.
  • Read from Environment Variables (12-Factor App methodology).
  • Use libraries like viper or kelseyhightower/envconfig.

6. Error Handling

6.1 Inspectable Errors

  • Never use panic for standard error flow.
  • Use %w to wrap errors to add context while preserving type.
if err := db.Query(); err != nil {
    return fmt.Errorf("querying user failed: %w", err)
}

6.2 Checking Errors

  • Use errors.Is() for value comparisons.
  • Use errors.As() for type assertions.
  • Never use _ to ignore an error. If an error is truly ignorable, document why.

6.3 Indentation (Line of Sight)

Handle errors early and return. Avoid else blocks after error checks.

// Bad
if err == nil {
    // heavy logic
} else {
    return err
}

// Good
if err != nil {
    return err
}
// heavy logic

7. Concurrency

7.1 Lifecycle Management

  • Never start a goroutine without knowing how it will stop.
  • Use context.Context for cancellation and timeout propagation.

7.2 Communication

  • "Share memory by communicating, don't communicate by sharing memory."
  • Use Channels for passing data ownership.
  • Use Mutexes (sync.Mutex) for protecting state integrity within a struct.

7.3 Context Usage

  • ctx should always be the first parameter of a function.
  • Never store Context inside a struct definition; pass it through the call stack.

8. Performance & Memory

8.1 Pointers vs Values

  • Use Pointers (T):
    • If you need to modify the receiver.
    • If the struct is large (> 64 bytes) to avoid copying.
    • If the struct contains a Mutex (mutexes must strictly not be copied).
  • Use Values (T):
    • For small structs, basic types, maps, and funcs.
    • To ensure immutability.
    • Note: Passing by value is often faster due to stack allocation logic.

8.2 Slice Allocation

If the length is known, pre-allocate slices to avoid resizing overhead.

// Good
users := make([]User, 0, len(ids))

9. Testing

9.1 Table-Driven Tests

Use table-driven tests for all logic-heavy functions.

func TestAdd(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive", 1, 2, 3},
        {"negative", -1, -1, -2},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Add(tt.a, tt.b); got != tt.want {
                t.Errorf("Add() = %v, want %v", got, tt.want)
            }
        })
    }
}

9.2 Test Packages

Use package foo_test (external testing) to ensure you are testing the public API of your package, preventing tight coupling to internal implementation details.

9.3 Race Detection

  • Mandatory in CI: All test pipelines must run with the -race flag enabled (go test -race ./...).
  • Local Development: Developers should run race detection locally when working on concurrent code.
  • Zero Tolerance: Any race condition reported by the tool is considered a critical bug and blocks merging.

9.4 Code Coverage

  • Target: We aim for 80% code coverage on core business logic (services, domain logic).
  • Enforcement: Use go test -coverprofile to generate reports.
  • Philosophy: High coverage does not guarantee bug-free code, but low coverage guarantees untested paths. Do not write assertions just to satisfy the counter; test behavior, not lines.

10. Observability (Logging & Metrics)

  • Structured Logging: Use log/slog (Go 1.21+) or zap.
  • Levels: Use strictly defined levels (DEBUG, INFO, WARN, ERROR).
  • No Printf: Do not use fmt.Println in production code.

11. Linting Configuration

We use golangci-lint. The following linters are mandatory:

# .golangci.yml snippet
linters:
  enable:
    - errcheck    # checking for unchecked errors
    - gosimple    # simplifies code
    - govet       # reports suspicious constructs
    - staticcheck # massive set of static analysis checks
    - unused      # checks for unused constants, variables, functions
    - bodyclose   # checks whether HTTP response body is closed
    - noctx       # finds sending http request without context.Context
    - revive      # fast, configurable, extensible, flexible, and beautiful linter

Document - Go Lang